/**
 * 
 */
package neptune.manager.builder;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;

import neptune.manager.NeptuneManager;
import neptune.manager.model.experiment.Experiment;
import neptune.manager.model.topology.IPAddress;
import neptune.manager.model.topology.NetworkInterface;
import neptune.manager.model.topology.Node;
import neptune.manager.model.topology.Topology;
import neptune.manager.storage.ExperimentRepositoryException;
import neptune.manager.utils.BitUtils;
import neptune.manager.utils.DebugLog;
import neptune.manager.utils.IllegalValueException;
import neptune.manager.utils.UnsignedByte;

/**
 * @author Roberto Bifulco [info@RobertoBifulco.it] [RobertoBifulco.it]
 * 
 */
public class IPAddressBuilder {

	/**
	 * Costruisce un indirizzo IP univoco, appartenente alla subnet passata come
	 * argomento.
	 * 
	 * @param subnetAddress
	 * @return
	 * @throws BuilderException
	 * @throws ExperimentRepositoryException
	 */
	public static IPAddress buildUniqueIP(IPAddress subnetAddress)
			throws BuilderException {
		return buildUniqueIP(subnetAddress, null);
	}

	/**
	 * Costruisce un indirizzo IP univoco, appartenente alla subnet passata come
	 * argomento. L'array di indirizzi passato come argomento, indica quali
	 * indirizzi escludere dalla generazione.
	 * 
	 * @param subnetAddress
	 * @param exclude
	 * @return
	 * @throws BuilderException
	 * @throws ExperimentRepositoryException
	 */
	public static IPAddress buildUniqueIP(IPAddress subnetAddress,
			IPAddress[] exclude) throws BuilderException {

		if (!subnetAddress.isSubnetAddress()) {
			DebugLog.printDetailedLog("buildUniqueIP: the argoument "
					+ "subnetAddress is not a subnet address: " + subnetAddress
					+ " " + subnetAddress.getSubnetMask(),
					IPAddressBuilder.class.getSimpleName());
			throw new BuilderException(
					"buildUniqueIP: the argoument subnetAddress is not a subnet address!");
		}

		try {
			ArrayList<IPAddress> usedAddresses = getUsedIPAddressesOnSystem(null);
			if (exclude != null)
				usedAddresses.addAll(Arrays.asList(exclude));

			IPAddress address = buildNextValidHostAddress(subnetAddress);
			while (address != null && usedAddresses.contains(address)) {
				address = buildNextValidHostAddress(address);
				DebugLog.printDetailedLog("IPAddress generation: " + address
						+ " generated", IPAddressBuilder.class.getSimpleName());
			}

			return address;
		} catch (ExperimentRepositoryException e) {
			throw new BuilderException(e);
		}
	}

	/**
	 * Costruisce un indirizzo di subnet, come subnet dell'indirizzo di subnet
	 * passato come argomento.<br>
	 * <br>
	 * Il metodo garantisce che non ci sia nessun indirizzo ip attivo sul
	 * sistema che si trovi sulla subnet generata.
	 * 
	 * 
	 * @param subnetAddress
	 * @param subnetMask
	 *            la subnetMask della subnet da generare
	 * @return
	 * @throws ExperimentRepositoryException
	 */
	public static IPAddress buildUniqueSubnetAddress(IPAddress subnetAddress,
			String subnetMask) throws ExperimentRepositoryException {
		return buildUniqueSubnetAddress(subnetAddress, subnetMask, null);
	}

	/**
	 * Costruisce un indirizzo di subnet, come subnet dell'indirizzo di subnet
	 * passato come argomento. La subnet generata avra' come subnetMask quella
	 * passata come argomento al metodo.<br>
	 * <br>
	 * Il metodo garantisce che non ci sia nessun indirizzo ip attivo sul
	 * sistema o sulla topology passata come argomento, che si trovi sulla
	 * subnet generata.
	 * 
	 * @param subnetAddress
	 * @param subnetMask
	 *            la subnetMask della subnet da generare
	 * @param topology
	 * @return
	 * @throws ExperimentRepositoryException
	 */
	public static IPAddress buildUniqueSubnetAddress(IPAddress subnetAddress,
			String subnetMask, Topology topology)
			throws ExperimentRepositoryException {
		try {
			/*
			 * Controlla che la subnet da generare abbia una subnet mask piu'
			 * lunga della subnet da cui deve essere generata
			 */
			BitSet maskBits = BitUtils.getBitValue(subnetAddress
					.getSubnetAddressInByte());
			int baseSubnetPrefixLenght = maskBits.cardinality();

			BitSet maskBits2 = BitUtils.getBitValue(IPAddress
					.toUnsignedBytes(subnetMask));
			int toGenerateSubnetPrefixLenght = maskBits2.cardinality();

			if (toGenerateSubnetPrefixLenght < baseSubnetPrefixLenght)
				return null;
			/*
			 * Fine controllo ----
			 */

			ArrayList<IPAddress> subnets = getUsedSubnetOnSystem(topology);

			IPAddress address = new IPAddress(4, subnetAddress.getAddress(),
					subnetMask).getSubnetAddress();

			while (subnets.contains(address)) {
				address = getNextSubnet(address);
				DebugLog.printDetailedLog("Subnet generation: " + address
						+ " generated", IPAddressBuilder.class.getSimpleName());
				if (address == null)
					return null;

				if (!subnetAddress.include(address))
					return null;
			}

			return address;
		} catch (IllegalValueException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Restituisce una lista gli indirizzi ip attualmente adoperati dal sistema
	 * a cui aggiunge quelli adoperati dalla topologia passata come argomento
	 * 
	 * @return
	 * @throws ExperimentRepositoryException
	 */
	public static ArrayList<IPAddress> getUsedIPAddressesOnSystem(
			Topology topology) throws ExperimentRepositoryException {

		ArrayList<IPAddress> addresses = new ArrayList<IPAddress>(0);

		NeptuneManager nm = NeptuneManager.instance();
		Experiment[] exps = nm.getExperiments();

		for (Experiment experiment : exps) {
			Topology top = experiment.getTopology();
			for (Node node : top.getNodes()) {
				for (NetworkInterface netIf : node.getInterfaces()) {
					for (IPAddress ipAddr : netIf.getIpAddresses()) {
						if (!addresses.contains(ipAddr))
							addresses.add(ipAddr);
					}
				}
			}
		}

		if (topology != null) {
			Topology top = topology;
			for (Node node : top.getNodes()) {
				for (NetworkInterface netIf : node.getInterfaces()) {
					for (IPAddress ipAddr : netIf.getIpAddresses()) {
						if (!addresses.contains(ipAddr))
							addresses.add(ipAddr);
					}
				}
			}
		}

		return addresses;
	}

	/**
	 * Restituisce una lista con le subnet attualmente adoperate dal sistema
	 * 
	 * @return
	 * @throws ExperimentRepositoryException
	 */
	public static ArrayList<IPAddress> getUsedSubnetOnSystem()
			throws ExperimentRepositoryException {
		return getUsedSubnetOnSystem(null);
	}

	/**
	 * Restituisce una lista con le subnet attualmente adoperate dal sistema a
	 * cui aggiunge quelle adoperate dalla topologia passata come argomento
	 * 
	 * @return
	 * @throws ExperimentRepositoryException
	 */
	public static ArrayList<IPAddress> getUsedSubnetOnSystem(Topology topology)
			throws ExperimentRepositoryException {

		ArrayList<IPAddress> subnets = new ArrayList<IPAddress>(0);

		NeptuneManager nm = NeptuneManager.instance();
		Experiment[] exps = nm.getExperiments();

		for (Experiment experiment : exps) {
			Topology top = experiment.getTopology();
			for (Node node : top.getNodes()) {
				for (NetworkInterface netIf : node.getInterfaces()) {
					for (IPAddress ipAddr : netIf.getIpAddresses()) {
						if (!subnets.contains(ipAddr.getSubnetAddress()))
							subnets.add(ipAddr.getSubnetAddress());
					}
				}
			}
		}

		if (topology != null) {
			Topology top = topology;
			for (Node node : top.getNodes()) {
				for (NetworkInterface netIf : node.getInterfaces()) {
					for (IPAddress ipAddr : netIf.getIpAddresses()) {
						if (!subnets.contains(ipAddr.getSubnetAddress()))
							subnets.add(ipAddr.getSubnetAddress());
					}
				}
			}
		}

		return subnets;
	}

	/**
	 * Restituisce il successivo indirizzo ip in base all'indirizzo ip passato
	 * come argomento. Se non esiste un successivo indirizzo sulla stessa
	 * subnet, restituisce null.
	 * 
	 * @param actualAddr
	 * @return
	 */
	public static IPAddress buildNextValidHostAddress(IPAddress actualAddr) {
		try {
			BitSet addr = BitUtils.getBitValue(actualAddr.getUnsignedBytes());

			if (BitUtils.incrementBits(addr, 32, 32))
				return null; // l'incremento ha causato overflow

			String addrString = IPAddress.toAddressString(IPAddress
					.getAddressBytesFromBits(addr));

			IPAddress newAddr = new IPAddress(4, addrString, actualAddr
					.getSubnetMask());

			IPAddress newAddrSubnet = newAddr.getSubnetAddress();
			IPAddress baseAddrSubnet = actualAddr.getSubnetAddress();

			if (newAddr.isSubnetAddress()) {
				// L'indirizzo deve essere un indirizzo host
				return null;
			} else if (!newAddrSubnet.equals(baseAddrSubnet)) {
				// L'indirizzo deve essere sulla stessa subnet della base
				return null;
			} else if (buildNextAddress(newAddr) == null) {
				// restituisce null se l'indirizzo generato e' quello di
				// broadcast
				return null;
			}

			return newAddr;
		} catch (IllegalValueException e) {
			throw new RuntimeException(e);
		}
	}

	/*
	 * Costruisce il successivo IPAddress sulla stessa subnet dell'argomento
	 * passato, senza verificare se e' un indirizzo di broadcast o di subnet e
	 * non di host.
	 * 
	 * @param actualAddr
	 * 
	 * @return
	 */
	private static IPAddress buildNextAddress(IPAddress actualAddr) {
		try {
			BitSet addr = BitUtils.getBitValue(actualAddr.getUnsignedBytes());

			if (BitUtils.incrementBits(addr, 32, 32))
				return null; // l'incremento ha causato overflow

			String addrString = IPAddress.toAddressString(IPAddress
					.getAddressBytesFromBits(addr));

			IPAddress newAddr = new IPAddress(4, addrString, actualAddr
					.getSubnetMask());

			IPAddress newAddrSubnet = newAddr.getSubnetAddress();
			IPAddress baseAddrSubnet = actualAddr.getSubnetAddress();

			if (!newAddrSubnet.equals(baseAddrSubnet)) {
				// L'indirizzo deve essere sulla stessa subnet della base
				return null;
			}

			return newAddr;
		} catch (IllegalValueException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Restituisce il successivo indirizzo di subnet rispetto alla subnet
	 * passata. Se non esiste un successivo indirizzo, restituisce null.
	 * 
	 * @param actualSubnet
	 * @return
	 */
	public static IPAddress getNextSubnet(IPAddress actualSubnet) {

		try {
			UnsignedByte[] mask = IPAddress.toUnsignedBytes(actualSubnet
					.getSubnetMask());

			BitSet maskBits = BitUtils.getBitValue(mask);
			int experimentPrefixLenght = maskBits.cardinality();

			BitSet addr = BitUtils.getBitValue(actualSubnet.getUnsignedBytes());

			if (BitUtils.incrementBits(addr, experimentPrefixLenght, 32))
				return null; // l'incremento ha causato overflow

			String addrString = IPAddress.toAddressString(IPAddress
					.getAddressBytesFromBits(addr));

			return new IPAddress(4, addrString, actualSubnet.getSubnetMask());
		} catch (IllegalValueException e) {
			throw new RuntimeException(e);
		}
	}
}
